# -*- coding: utf-8 -*-
# @Time    : 2024/7/26 10:18
# @Author  : yujiahao
# @File    : 61_fastapi_http_basic_auth.py
# @description:HTTP 基础授权


"""
HTTP 基础授权的简单用例总结

1. 基本概念：
   - 最简单的用例是使用 HTTP 基础授权（HTTP Basic Auth）。
   - 在 HTTP 基础授权中，应用需要请求头包含用户名与密码。
   - HTTP 基础授权让浏览器显示内置的用户名与密码提示。
   - 输入用户名与密码后，浏览器会把它们自动发送至请求头。

2. 错误处理：
   - 如果没有接收到 HTTP 基础授权，就返回 HTTP 401 "Unauthorized" 错误。
   - 并返回含 Basic 值的请求头 WWW-Authenticate 以及可选的 realm 参数。

3. 实现步骤：简单的 HTTP 基础授权
   - 导入 HTTPBasic 与 HTTPBasicCredentials。
   - 使用 HTTPBasic 创建安全概图。
   - 在路径操作的依赖项中使用 security。
   - 返回类型为 HTTPBasicCredentials 的对象，包含发送的 username 与 password。
"""
import secrets
# 用于类型注解，结合依赖项注入。
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
# 用于处理 HTTP 基本认证。
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

# 定义 HTTP 基本认证的安全依赖项
"""
    - HTTPBasic 是 FastAPI 提供的一个类，用于处理 HTTP 基本认证。
    - 创建一个 security 实例，这个实例将用于路径操作函数中的依赖项注入。
"""
security = HTTPBasic()

# 使用依赖项检查用户名与密码是否正确。

# 这里说一下时差攻击
# todo secrets.compare_digest()
"""
1. 时差攻击简介：
   - 时差攻击是一种基于测量服务器响应时间差异来猜测正确用户名和密码的攻击方法。

2. 时差攻击示例：
   - 假设攻击者试图猜出用户名与密码。
   - 他们发送用户名为 'johndoe'，密码为 'love123' 的请求。
   - Python 代码如下：
     if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
         ...
   - 在比较 'johndoe' 和 'stanleyjobson' 的第一个字母时，Python 知道这两个字符串不相同，立即返回错误。

3. 攻击者的进一步尝试：
   - 攻击者继续尝试 'stanleyjobsox' 和密码 'love123'。
   - Python 代码如下：
     if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
         ...
   - 此时，Python 要对比 'stanleyjobsox' 和 'stanleyjobson' 中的 'stanleyjobso'，才能知道这两个字符串不一样，因此会多花费几微秒。

4. 反应时间对攻击者的帮助：
   - 通过服务器花费的额外时间，攻击者可以知道猜对了一些内容。
   - 他们可以放弃 'johndoe'，继续尝试类似 'stanleyjobsox' 的内容。

5. 专业攻击：
   - 攻击者可以编写每秒执行成千上万次测试的程序，逐步猜出正确的用户名和密码。

6. 使用 secrets.compare_digest() 修补：
   - 使用 secrets.compare_digest() 进行比较可以防止时差攻击。
   - 它使用相同的时间对比任意两个字符串，防止通过响应时间差异进行攻击。

7. 在代码中使用 secrets.compare_digest()：
   - 使用 secrets.compare_digest() 来安全地防御全面攻击。
"""

"""
1. 使用依赖项检查用户名与密码是否正确。
2. 为此要使用 Python 标准模块 secrets 检查用户名与密码。
3. secrets.compare_digest() 需要仅包含 ASCII 字符（英语字符）的 bytes 或 str，这意味着它不适用于像á一样的字符，如 Sebastián。
4. 为了解决这个问题，我们首先将 username 和 password 转换为使用 UTF-8 编码的 bytes 。
5. 然后我们可以使用 secrets.compare_digest() 来确保 credentials.username 是 "stanleyjobson"，且 credentials.password 是"swordfish"。
"""


def get_current_username(
        # 使用 Annotated 和 Depends 声明依赖项，获取 HTTP 基本认证的凭据
        credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    # 将提供的用户名编码为字节类型
    current_username_bytes = credentials.username.encode("utf8")
    # 定义正确的用户名（字节类型）
    correct_username_bytes = b"stanleyjobson"
    # 使用 secrets.compare_digest 比较提供的用户名和正确的用户名
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )

    # 将提供的密码编码为字节类型
    current_password_bytes = credentials.password.encode("utf8")
    # 定义正确的密码（字节类型）
    correct_password_bytes = b"swordfish"
    # 使用 secrets.compare_digest 比较提供的密码和正确的密码
    # 使用 secrets.compare_digest()，可以防御时差攻击，更加安全。
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )

    # 如果用户名或密码不正确，抛出 HTTP 401 未授权异常
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )

    # 如果用户名和密码都正确，返回用户名
    return credentials.username


@app.get("/read_current_user/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}


@app.get("/users/me")
# credentials 是路径操作函数的参数，类型为 HTTPBasicCredentials
# Depends(security) 表示 credentials 参数的值将由 security 提供，即 HTTPBasic 认证机制
# Annotated 用于结合类型注解和依赖项注入。
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
    """
    当客户端向 /users/me 发送请求时，服务器会要求客户端提供 HTTP 基本认证的凭据（用户名和密码）。

    1. 客户端请求：
       - 客户端在请求头中包含 Authorization 字段，格式为 Basic base64(username:password)。

    2. 服务器端处理：
       - FastAPI 的 HTTPBasic 解析这个 Authorization 字段。
       - 将用户名和密码封装在 HTTPBasicCredentials 对象中。

    3. HTTPBasicCredentials 对象：
       - 包含两个属性：
         - username：用户名。
         - password：密码。
    """
    return {"username": credentials.username, "password": credentials.password}
